/*******************************************************************************
 * Copyright (c) 2007 University of Oslo.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     University of Oslo - initial API and implementation
 *******************************************************************************/

package com.cea.papyrus.diagram.sequence.sedi.figures;

import static java.lang.Math.abs;
import static java.lang.Math.max;

import org.eclipse.draw2d.BorderLayout;
import org.eclipse.draw2d.FlowLayout;
import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.Label;
import org.eclipse.draw2d.LineBorder;
import org.eclipse.draw2d.Panel;
import org.eclipse.draw2d.ToolbarLayout;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;

import com.cea.papyrus.diagraminterchange2.di2.GraphElement;

/**
 * Figure for Lifelines.
 * 
 * @author Andreas Limyr
 * @author frankd@ifi.uio.no
 */
public class LifelineFigure extends AnchoringFigure {

    private static final int MIN_HEIGHT = 50;
    private static final Dimension DEFAULT_SIZE = new Dimension(100, 320);
    private static final int MIN_WIDTH = 50;

    // Vertical margin above and below the lifeline label(s)
    private static final int LABEL_V_MARGIN = 4;

    // The height of the lifeline label box. This value is calculated
    // based on font size on first access -- see #validate()
    public static int HEAD_HEIGHT = 0;

    // The area (to the left and right) of the lifeline that is sensible to the mouse
    private static final int LIFELINE_HIT_WIDTH = 6;

    public static final int VERTICAL_POSITION = 40;

    // Defines the visual appearance of lifelines
    private static int[] LIFELINE_DASH_STYLE = new int[] {
            5, 5
    };

    private Label textLabel;
    private Label refLabel;
    private Label stereotypeLabel;
    private Panel lifelineHead;
    private Panel refPanel;

    public LifelineFigure() {
        setLayoutManager(new BorderLayout());

        // Centering text within the lifeline head is not too easy to
        // do using only GEF classes. GMF contains a ConstrainedToolbarLayout
        // which would do the job, but it would add a heavy dependency for
        // very little gain. Therefore we replicate the class here.
        ConstrainedToolbarLayout headLayout = new ConstrainedToolbarLayout(ToolbarLayout.VERTICAL);
        headLayout.setMinorAlignment(ToolbarLayout.ALIGN_CENTER);
        headLayout.setStretchMajorAxis(false);
        headLayout.setMajorAlignment(ToolbarLayout.ALIGN_CENTER);

        lifelineHead = new Panel();
        lifelineHead.setBorder(new LineBorder(1));
        lifelineHead.setLayoutManager(headLayout);

        add(lifelineHead, BorderLayout.TOP);

        stereotypeLabel = new Label();

        textLabel = new Label();
        lifelineHead.add(textLabel);

        FlowLayout refLayout = new FlowLayout(FlowLayout.HORIZONTAL);
        refLayout.setMajorAlignment(FlowLayout.ALIGN_CENTER);
        refLayout.setMinorAlignment(FlowLayout.ALIGN_CENTER);
        refPanel = new Panel();
        refPanel.setLayoutManager(refLayout);
        refPanel.add(new Label("ref "));

        refLabel = new Label();
        refPanel.add(refLabel);
    }

    public Rectangle getPreferredBounds(Rectangle suggestedBounds) {
        // TODO The top y should depend on any present local attribute figures
        // Lifelines should also not overlap
        int y = (suggestedBounds.y < 70) ? 40 : suggestedBounds.y;
        int width, height;
        if (suggestedBounds.width == 0) {
            width = DEFAULT_SIZE.width;
            height = getParent().getBounds().height - y - 15;
        } else {
            width = max(suggestedBounds.width, MIN_WIDTH);
            height = max(suggestedBounds.height, MIN_HEIGHT);
        }
        return new Rectangle(suggestedBounds.x, y, width, height);
    }

    @Override
    protected void paintFigure(Graphics graphics) {
        textLabel.setFont(this.getFont());
        refLabel.setFont(this.getFont());
        stereotypeLabel.setFont(this.getFont());
        textLabel.setForegroundColor(this.getFontColor());
        refLabel.setForegroundColor(this.getFontColor());
        stereotypeLabel.setForegroundColor(this.getFontColor());
        lifelineHead.setForegroundColor(this.getBorderColor());
        lifelineHead.setFont(this.getFont());
        Rectangle rect = getBounds().getCopy();
        int middle = rect.width / 2;
        graphics.setForegroundColor(this.getBorderColor());
        graphics.setLineStyle(Graphics.LINE_CUSTOM);
        graphics.setLineDash(LIFELINE_DASH_STYLE);

        graphics
                .drawLine(rect.x + middle, rect.y + HEAD_HEIGHT, rect.x + middle, rect.bottom() - 1);

        // setting the linedash attribute has a sideeffect on any connection
        // figures drawn later. The original value is 'null', which we can't
        // restore due to the implementation of setLineDash method. Fortunately
        // we can _almost_ restore it this way:
        // FIXME Talk with people about this problem
        graphics.setLineStyle(Graphics.LINE_SOLID);
        graphics.setLineDash(new int[] {});
    }

    @Override
    public void validate() {
        if (isValid())
            return;

        super.validate();

        // By now child labels are laid out by GEF, and we can query it
        // to determine the actual height of the lifeline "box" in such
        // as way as to neatly fit the text label(s).
        // Only do this once though.
        if (HEAD_HEIGHT == 0)
            HEAD_HEIGHT = 3 * textLabel.getBounds().height + 2 * LABEL_V_MARGIN;

        if (lifelineHead.getPreferredSize().height != HEAD_HEIGHT) {
            lifelineHead.setPreferredSize(new Dimension(-1, HEAD_HEIGHT));
        }
        updateAnchors();
    }

    @Override
    public boolean containsPoint(int x, int y) {
        Rectangle rect = getBounds();

        // Check if point is inside head!
        // TODO This task can be delegated to the root panel
        // but in that case, coordinates have to be converted to local
        if (x > rect.x && x < rect.x + rect.width && y > rect.y && y < rect.y + HEAD_HEIGHT - 1)
            return true;

        return (x > (rect.x + rect.width / 2 - LIFELINE_HIT_WIDTH)
                && x < (rect.x + rect.width / 2 + LIFELINE_HIT_WIDTH) && y > rect.y && y < rect.y
                + rect.height);
    }

    public static final int TARGET_NAME = 1;
    public static final int TARGET_REF = 2;

    /**
     * Determines which label (the lifeline name or the lifeline decomposition name) owns
     * the given point.
     * 
     * @param location
     * @return TARGET_NAME (default) or TARGET_REF
     */
    public int findEditTargetAt(Point location) {
        if (location == null)
            return TARGET_NAME;

        Rectangle bounds = getRefBounds();
        return (getRefText() != null && bounds.contains(location)) ? TARGET_REF : TARGET_NAME;
    }

    @Override
    protected boolean useLocalCoordinates() {
        return true;
    }

    // --- ILabeledFigure ---

    public Rectangle getLabelBounds() {
        Rectangle rect = textLabel.getClientArea().getCopy();
        textLabel.translateToAbsolute(rect);
        return rect;
    }

    public void setLabel(String labelText) {
        textLabel.setText(labelText);
    }

    // extensions due to extra decomposition label

    public String getRefText() {
        return refLabel.getText();
    }

    public void setRefText(final String newText) {
        if (newText == null && lifelineHead.getChildren().contains(refPanel))
            lifelineHead.remove(refPanel);
        else if (newText != null && !lifelineHead.getChildren().contains(refPanel))
            lifelineHead.add(refPanel);

        refLabel.setText(newText);
        invalidate();
    }

    public void setStereotypes(final String label) {
        if (label == null && lifelineHead.getChildren().contains(stereotypeLabel))
            lifelineHead.remove(stereotypeLabel);
        else if (label != null && !lifelineHead.getChildren().contains(stereotypeLabel))
            lifelineHead.add(stereotypeLabel, 0);

        stereotypeLabel.setText(label);
        invalidate();
    }

    public Rectangle getRefBounds() {
        Rectangle rect = refLabel.getClientArea().getCopy();
        refLabel.translateToAbsolute(rect);
        return rect;
    }

    // --- Used by Coverable analysis ---

    /**
     * Calculates the horizontal distance from this lifeline to the center of the given
     * rectangle.
     * 
     * @param fragmentBounds
     *            The bounds whose distance is tested. The coordinates must be given in
     *            absolute coordinates.
     * @returns the (always positive) horizontal distance.
     */
    public int getDistanceTo(Rectangle fragmentBounds) {
        Rectangle lifelineBounds = Rectangle.SINGLETON;
        lifelineBounds.setBounds(getBounds());
        translateToAbsolute(lifelineBounds);
        return abs(fragmentBounds.x - lifelineBounds.x
                + (fragmentBounds.width - lifelineBounds.width) / 2);
    }

    /**
     * Tests whether the given rectangle covers this lifeline.
     * 
     * @param fragmentBounds
     *            The bounds whose distance is tested. The coordinates must be given
     *            either in absolute coordinates or in interaction coordinates.
     * @param local
     *            if true, the coordinates are given in interaction-local coordinates. if
     *            false, they are absolute coordinates. TODO Always use absolute
     *            coordinates
     */
    public boolean isCovered(final Rectangle fragmentBounds, final boolean local) {
        final Rectangle lifelineBounds = Rectangle.SINGLETON;
        lifelineBounds.setBounds(getBounds());
        if (!local) {
            // translateToParent(lifelineBounds);
            translateToAbsolute(lifelineBounds);
        }

        final int lifelineX = lifelineBounds.x + lifelineBounds.width / 2;
        return (lifelineX >= fragmentBounds.x && lifelineX < fragmentBounds.right());
    }

    // --- Anchor contribution ---

    private AnchorPolicy anchorPolicy;

    public AnchorPolicy getAnchorPolicy() {
        if (anchorPolicy == null)
            anchorPolicy = new LifelineAnchorPolicy();
        return anchorPolicy;
    }

    public Rectangle getAnchorContributionBounds(IFigure figure) {
        return new Rectangle(0, 0, getBounds().width, getBounds().height);
        // return new Rectangle(0, HEAD_HEIGHT + Grid.RESERVE,
        // getSize().width, getSize().height - HEAD_HEIGHT - Grid.RESERVE);
    }

    public int[] contributeAnchors(IFigure figure, int y) {
        int headmiddle = (HEAD_HEIGHT - Grid.ANCHOR_DELTA) / 2;
        if (y >= headmiddle && y < headmiddle + Grid.ANCHOR_DELTA)
            return new int[] {
                    0, getSize().width
            };
        else if (y >= HEAD_HEIGHT + Grid.RESERVE)
            return new int[] {
                getSize().width / 2
            };
        else
            return AnchorContributor.NO_CONTRIBUTIONS;
    }
    
    /**
     * @see com.cea.papyrus.diagram.sequence.sedi.figures.SeDiSubpartFigure#refreshStereotypes(com.cea.papyrus.diagraminterchange2.di2.GraphElement)
     */
    @Override
    public void refreshStereotypes(GraphElement graphElement) {
        // Does nothing, stereotypes of lifelines are already managed by SeDi 
    }
}
